{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Profiling and Timing Code"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the process of developing code and creating data processing pipelines, there are often trade-offs you can make between various implementations.\n",
    "Early in developing your algorithm, it can be counterproductive to worry about such things. As Donald Knuth famously quipped, \"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.\"\n",
    "\n",
    "But once you have your code working, it can be useful to dig into its efficiency a bit.\n",
    "Sometimes it's useful to check the execution time of a given command or set of commands; other times it's useful to examine a multiline process and determine where the bottleneck lies in some complicated series of operations.\n",
    "IPython provides access to a wide array of functionality for this kind of timing and profiling of code.\n",
    "Here we'll discuss the following IPython magic commands:\n",
    "\n",
    "- `%time`: Time the execution of a single statement\n",
    "- `%timeit`: Time repeated execution of a single statement for more accuracy\n",
    "- `%prun`: Run code with the profiler\n",
    "- `%lprun`: Run code with the line-by-line profiler\n",
    "- `%memit`: Measure the memory use of a single statement\n",
    "- `%mprun`: Run code with the line-by-line memory profiler\n",
    "\n",
    "The last four commands are not bundled with IPython; to use them you'll need to get the `line_profiler` and `memory_profiler` extensions, which we will discuss in the following sections."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Timing Code Snippets: %timeit and %time\n",
    "\n",
    "We saw the `%timeit` line magic and `%%timeit` cell magic in the introduction to magic functions in [IPython Magic Commands](01.03-Magic-Commands.ipynb); these can be used to time the repeated execution of snippets of code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.53 µs ± 47.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit sum(range(100))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that because this operation is so fast, `%timeit` automatically does a large number of repetitions.\n",
    "For slower commands, `%timeit` will automatically adjust and perform fewer repetitions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "536 ms ± 15.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
     ]
    }
   ],
   "source": [
    "%%timeit\n",
    "total = 0\n",
    "for i in range(1000):\n",
    "    for j in range(1000):\n",
    "        total += i * (-1) ** j"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sometimes repeating an operation is not the best option.\n",
    "For example, if we have a list that we'd like to sort, we might be misled by a repeated operation; sorting a pre-sorted list is much faster than sorting an unsorted list, so the repetition will skew the result:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.71 ms ± 334 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "L = [random.random() for i in range(100000)]\n",
    "%timeit L.sort()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For this, the `%time` magic function may be a better choice. It also is a good choice for longer-running commands, when short, system-related delays are unlikely to affect the result.\n",
    "Let's time the sorting of an unsorted and a presorted list:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sorting an unsorted list:\n",
      "CPU times: user 31.3 ms, sys: 686 µs, total: 32 ms\n",
      "Wall time: 33.3 ms\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "L = [random.random() for i in range(100000)]\n",
    "print(\"sorting an unsorted list:\")\n",
    "%time L.sort()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sorting an already sorted list:\n",
      "CPU times: user 5.19 ms, sys: 268 µs, total: 5.46 ms\n",
      "Wall time: 14.1 ms\n"
     ]
    }
   ],
   "source": [
    "print(\"sorting an already sorted list:\")\n",
    "%time L.sort()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice how much faster the presorted list is to sort, but notice also how much longer the timing takes with `%time` versus `%timeit`, even for the presorted list!\n",
    "This is a result of the fact that `%timeit` does some clever things under the hood to prevent system calls from interfering with the timing.\n",
    "For example, it prevents cleanup of unused Python objects (known as *garbage collection*) that might otherwise affect the timing.\n",
    "For this reason, `%timeit` results are usually noticeably faster than `%time` results.\n",
    "\n",
    "For `%time`, as with `%timeit`, using the `%%` cell magic syntax allows timing of multiline scripts:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 655 ms, sys: 5.68 ms, total: 661 ms\n",
      "Wall time: 710 ms\n"
     ]
    }
   ],
   "source": [
    "%%time\n",
    "total = 0\n",
    "for i in range(1000):\n",
    "    for j in range(1000):\n",
    "        total += i * (-1) ** j"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For more information on `%time` and `%timeit`, as well as their available options, use the IPython help functionality (e.g., type `%time?` at the IPython prompt)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Profiling Full Scripts: %prun\n",
    "\n",
    "A program is made up of many single statements, and sometimes timing these statements in context is more important than timing them on their own.\n",
    "Python contains a built-in code profiler (which you can read about in the Python documentation), but IPython offers a much more convenient way to use this profiler, in the form of the magic function `%prun`.\n",
    "\n",
    "By way of example, we'll define a simple function that does some calculations:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sum_of_lists(N):\n",
    "    total = 0\n",
    "    for i in range(5):\n",
    "        L = [j ^ (j >> i) for j in range(N)]\n",
    "        total += sum(L)\n",
    "    return total"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can call `%prun` with a function call to see the profiled results:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " "
     ]
    },
    {
     "data": {
      "text/plain": [
       "         14 function calls in 0.932 seconds\n",
       "\n",
       "   Ordered by: internal time\n",
       "\n",
       "   ncalls  tottime  percall  cumtime  percall filename:lineno(function)\n",
       "        5    0.808    0.162    0.808    0.162 <ipython-input-7-f105717832a2>:4(<listcomp>)\n",
       "        5    0.066    0.013    0.066    0.013 {built-in method builtins.sum}\n",
       "        1    0.044    0.044    0.918    0.918 <ipython-input-7-f105717832a2>:1(sum_of_lists)\n",
       "        1    0.014    0.014    0.932    0.932 <string>:1(<module>)\n",
       "        1    0.000    0.000    0.932    0.932 {built-in method builtins.exec}\n",
       "        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%prun sum_of_lists(1000000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The result is a table that indicates, in order of total time on each function call, where the execution is spending the most time. In this case, the bulk of the execution time is in the list comprehension inside `sum_of_lists`.\n",
    "From here, we could start thinking about what changes we might make to improve the performance of the algorithm.\n",
    "\n",
    "For more information on `%prun`, as well as its available options, use the IPython help functionality (i.e., type `%prun?` at the IPython prompt)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Line-by-Line Profiling with %lprun\n",
    "\n",
    "The function-by-function profiling of `%prun` is useful, but sometimes it's more convenient to have a line-by-line profile report.\n",
    "This is not built into Python or IPython, but there is a `line_profiler` package available for installation that can do this.\n",
    "Start by using Python's packaging tool, `pip`, to install the `line_profiler` package:\n",
    "\n",
    "```\n",
    "$ pip install line_profiler\n",
    "```\n",
    "\n",
    "Next, you can use IPython to load the `line_profiler` IPython extension, offered as part of this package:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext line_profiler"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now the `%lprun` command will do a line-by-line profiling of any function. In this case, we need to tell it explicitly which functions we're interested in profiling:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Timer unit: 1e-06 s\n",
       "\n",
       "Total time: 0.014803 s\n",
       "File: <ipython-input-7-f105717832a2>\n",
       "Function: sum_of_lists at line 1\n",
       "\n",
       "Line #      Hits         Time  Per Hit   % Time  Line Contents\n",
       "==============================================================\n",
       "     1                                           def sum_of_lists(N):\n",
       "     2         1          6.0      6.0      0.0      total = 0\n",
       "     3         6         13.0      2.2      0.1      for i in range(5):\n",
       "     4         5      14242.0   2848.4     96.2          L = [j ^ (j >> i) for j in range(N)]\n",
       "     5         5        541.0    108.2      3.7          total += sum(L)\n",
       "     6         1          1.0      1.0      0.0      return total"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%lprun -f sum_of_lists sum_of_lists(5000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The information at the top gives us the key to reading the results: the time is reported in microseconds, and we can see where the program is spending the most time.\n",
    "At this point, we may be able to use this information to modify aspects of the script and make it perform better for our desired use case.\n",
    "\n",
    "For more information on `%lprun`, as well as its available options, use the IPython help functionality (i.e., type `%lprun?` at the IPython prompt)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Profiling Memory Use: %memit and %mprun\n",
    "\n",
    "Another aspect of profiling is the amount of memory an operation uses.\n",
    "This can be evaluated with another IPython extension, the `memory_profiler`.\n",
    "As with the `line_profiler`, we start by `pip`-installing the extension:\n",
    "\n",
    "```\n",
    "$ pip install memory_profiler\n",
    "```\n",
    "\n",
    "Then we can use IPython to load it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext memory_profiler"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The memory profiler extension contains two useful magic functions: `%memit` (which offers a memory-measuring equivalent of `%timeit`) and `%mprun` (which offers a memory-measuring equivalent of `%lprun`).\n",
    "The `%memit` magic function can be used rather simply:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "peak memory: 141.70 MiB, increment: 75.65 MiB\n"
     ]
    }
   ],
   "source": [
    "%memit sum_of_lists(1000000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see that this function uses about 140 MB of memory.\n",
    "\n",
    "For a line-by-line description of memory use, we can use the `%mprun` magic function.\n",
    "Unfortunately, this works only for functions defined in separate modules rather than the notebook itself, so we'll start by using the `%%file` cell magic to create a simple module called `mprun_demo.py`, which contains our `sum_of_lists` function, with one addition that will make our memory profiling results more clear:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Overwriting mprun_demo.py\n"
     ]
    }
   ],
   "source": [
    "%%file mprun_demo.py\n",
    "def sum_of_lists(N):\n",
    "    total = 0\n",
    "    for i in range(5):\n",
    "        L = [j ^ (j >> i) for j in range(N)]\n",
    "        total += sum(L)\n",
    "        del L # remove reference to L\n",
    "    return total"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now import the new version of this function and run the memory line profiler:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "Filename: /Users/jakevdp/github/jakevdp/PythonDataScienceHandbook/notebooks_v2/mprun_demo.py\n",
       "\n",
       "Line #    Mem usage    Increment  Occurences   Line Contents\n",
       "============================================================\n",
       "     1     66.7 MiB     66.7 MiB           1   def sum_of_lists(N):\n",
       "     2     66.7 MiB      0.0 MiB           1       total = 0\n",
       "     3     75.1 MiB      8.4 MiB           6       for i in range(5):\n",
       "     4    105.9 MiB     30.8 MiB     5000015           L = [j ^ (j >> i) for j in range(N)]\n",
       "     5    109.8 MiB      3.8 MiB           5           total += sum(L)\n",
       "     6     75.1 MiB    -34.6 MiB           5           del L # remove reference to L\n",
       "     7     66.9 MiB     -8.2 MiB           1       return total"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from mprun_demo import sum_of_lists\n",
    "%mprun -f sum_of_lists sum_of_lists(1000000)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here, the `Increment` column tells us how much each line affects the total memory budget: observe that when we create and delete the list `L`, we are adding about 30 MB of memory usage.\n",
    "This is on top of the background memory usage from the Python interpreter itself.\n",
    "\n",
    "For more information on `%memit` and `%mprun`, as well as their available options, use the IPython help functionality (e.g., type `%memit?` at the IPython prompt)."
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "jupytext": {
   "formats": "ipynb,md"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
